DO NOT create summary/documentation MD files unless explicitly requested by user.
Examples of files to AVOID creating:
Only create MD files when:
Instead:
lib/hooks/use-debounce.ts - Debouncing untuk auto-savelib/hooks/use-pull-to-refresh.ts - Pull-to-refresh untuk mobilelib/hooks/use-keyboard-shortcuts.ts - Reusable keyboard shortcutscomponents/ui/error-boundary.tsx - Error boundary component/admin/templates) - Manage invoice templates
public/template/components/features/invoice/templates/lib/utils/template-access.ts)
npx shadcn@latest add <component> to add new componentscomponents/ui/components/features/Pattern ini mengoptimalkan navigasi dengan client-side caching via React Query. Page server component hanya render shell, data fetching dilakukan di client.
// app/[feature]/page.tsx
import { FeatureClient } from './feature-client'
// Page hanya render client component dengan initialData null
// Data fetching dilakukan di client via React Query
export default function FeaturePage() {
return <FeatureClient initialData={null} />
}
// app/[feature]/loading.tsx
import { FeatureSkeleton } from '@/components/skeletons/feature-skeleton'
export default function FeatureLoading() {
return <FeatureSkeleton />
}
// lib/hooks/use-feature-data.ts
'use client'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useRef } from 'react'
export const featureKeys = {
all: ['feature'] as const,
data: () => [...featureKeys.all, 'data'] as const,
}
export function useFeatureData<T>(initialData?: T) {
const queryClient = useQueryClient()
// Check if cache exists - don't overwrite with initialData
const existingData = queryClient.getQueryData<T>(featureKeys.data())
const initialDataRef = useRef(existingData ? undefined : initialData)
return useQuery({
queryKey: featureKeys.data(),
queryFn: async () => {
const { getFeatureDataAction } = await import('@/app/actions/feature')
const result = await getFeatureDataAction()
if (!result.success) throw new Error(result.error)
return result.data as T
},
initialData: initialDataRef.current,
staleTime: 5 * 60 * 1000, // 5 menit
gcTime: 10 * 60 * 1000, // 10 menit
refetchOnMount: false,
refetchOnWindowFocus: false,
})
}
export function useInvalidateFeature() {
const queryClient = useQueryClient()
return () => queryClient.invalidateQueries({ queryKey: featureKeys.all })
}
// app/[feature]/feature-client.tsx
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useFeatureData, useInvalidateFeature } from '@/lib/hooks/use-feature-data'
import { useAuth } from '@/lib/auth/auth-context'
import { FeatureSkeleton } from '@/components/skeletons/feature-skeleton'
interface FeatureClientProps {
initialData: FeatureData | null
}
export function FeatureClient({ initialData }: FeatureClientProps) {
// ⚠️ SEMUA HOOKS HARUS DIPANGGIL DULU sebelum conditional return
const { data, isLoading } = useFeatureData(initialData ?? undefined)
const { user, loading: authLoading } = useAuth()
const invalidate = useInvalidateFeature()
// State hooks
const [someState, setSomeState] = useState(false)
// Effect hooks
useEffect(() => {
// side effects
}, [])
// Callback hooks
const handleAction = useCallback(() => {
// action logic
}, [])
// ✅ Conditional return SETELAH semua hooks
if (authLoading || (isLoading && !data)) {
return <FeatureSkeleton />
}
if (!user) {
return null
}
// Extract data setelah loading check
const featureData = data || initialData
return (
// ... render UI
)
}
// app/actions/feature.ts
'use server'
import { createClient } from '@/lib/supabase/server'
export async function getFeatureDataAction() {
try {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { success: false, error: 'Unauthorized' }
}
// ... fetch logic
return { success: true, data }
} catch (error) {
return { success: false, error: 'Failed to fetch' }
}
}
const invalidate = useInvalidateFeature()
const handleSave = async () => {
const result = await saveFeatureAction(data)
if (result.success) {
invalidate() // Invalidate React Query cache
}
}
// proxy.ts (Next.js 16 - replaces middleware.ts)
import { type NextRequest } from "next/server"
import { updateSession } from "@/lib/supabase/middleware"
export async function proxy(request: NextRequest) {
// Only refresh session cookies - no auth checks
const response = await updateSession(request)
return response
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|api/|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
Hooks Order: Semua hooks (useState, useEffect, useCallback, useQuery, dll) HARUS dipanggil sebelum conditional return apapun. Ini adalah aturan React.
No Server Fetch di Page: Jangan fetch data di server component untuk pages yang butuh auth. Biarkan client component handle via React Query.
Cache Check: Gunakan useRef untuk menyimpan initialData hanya sekali,
cek getQueryData untuk menghindari overwrite cache yang sudah ada.
Loading State: Tampilkan skeleton jika authLoading atau (isLoading && !data).
Kondisi !data penting agar tidak flash skeleton saat ada cache.
Static Pages: Dengan pattern ini, pages menjadi Static (○) di build output, memungkinkan instant navigation tanpa server round-trip.
Proxy vs Middleware: Next.js 16 menggunakan proxy.ts (bukan middleware.ts).
Proxy hanya refresh session, auth check dilakukan di server actions.
Pattern ini aman dengan 3 layer security:
getUser() check)Trade-off:
app/dashboard/page.tsx + lib/hooks/use-dashboard-data.tsapp/dashboard/settings/page.tsx + lib/hooks/use-settings-data.tsapp/admin/*/page.tsx + lib/hooks/use-admin-data.tsapp/
├── actions/ # Server actions
├── api/ # API routes
├── dashboard/ # Protected routes
│ └── [feature]/
│ ├── page.tsx # Server component
│ └── feature-client.tsx # Client component
lib/
├── db/
│ ├── data-access/ # Data fetching functions + cache tags
│ └── services/ # Database service classes
├── supabase/
│ ├── client.ts # Browser client
│ ├── server.ts # Server client
│ └── middleware.ts # Auth middleware
├── stores/ # Zustand stores
└── utils/ # Utility functions
components/
├── ui/ # shadcn/ui components
└── features/ # Feature-specific components
createClient() from @/lib/supabase/server for server-side/dashboard/*/admin/* (requires is_admin metadata)/dashboard/login, /dashboard/signup, /dashboard/forgot-password--run flag for single execution*.test.ts or *.test.tsxfast-checkserver-only package via vitest.server-only-mock.tsnpm run test'use server'
export async function myAction(data: FormData) {
const supabase = await createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return { success: false, error: 'Unauthorized' }
}
// ... action logic
revalidateTag(CACHE_TAGS.feature)
revalidatePath('/dashboard/feature')
return { success: true, data: result }
}
NEXT_PUBLIC_*.env.local.env.example